package com.hyq.learn.api.util;

import com.google.common.collect.ImmutableRangeMap;
import com.google.common.collect.Range;
import com.google.common.collect.RangeMap;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * @author heyuqin
 * @date 2024/8/14 15:30
 */
public class EncodeUtil {

    private final static Logger LOGGER = LoggerFactory.getLogger(EncodeUtil.class);

    private final static RangeMap<Integer, Integer> CODE_POINT_HEX_UTF8_RANGE_MAP = ImmutableRangeMap.<Integer, Integer>builder()
            .put(Range.closed(0, 127), 1)
            .put(Range.closed(128, 2047), 2)
            .put(Range.closed(2048, 65535), 3)
            .put(Range.closed(65536, 1114111), 4).build();
    private final static RangeMap<Integer, Integer> CODE_POINT_HEX_UTF16_RANGE_MAP = ImmutableRangeMap.<Integer, Integer>builder()
            .put(Range.closed(0, 65535), 1)
            .put(Range.closed(65536, 1114111), 2).build();


    public static final byte MIN_VALUE = -128;

    /**
     * A constant holding the maximum value a {@code byte} can
     * have, 2<sup>7</sup>-1.
     */
    public static final byte MAX_VALUE = 127;

    private static final String SUPPLEMENT_NUMS = "000000";

    private static final Integer CODE_POINT_LENGTH = 6;

    private EncodeUtil() {
    }

    public static byte[] string2Bytes(String s) {
        return s.getBytes();
    }

    public static String string2Hex(String s) {
        byte[] bytes = string2Bytes(s);
        return Hex.encodeHexString(bytes);
    }

    public static String bytes2String(byte[] bytes) {
        return new String(bytes);
    }

    public static byte[] hex2Bytes(String hexString) {
        byte[] bytes;
        try {
            bytes = Hex.decodeHex(hexString.toCharArray());
        } catch (DecoderException e) {
            throw new RuntimeException(e);
        }
        return bytes;
    }

    public static String hex2BinaryStringWithCompensate(String hexString) {
        int i = Integer.parseInt(hexString, 16);
        StringBuilder binaryString = new StringBuilder(Integer.toBinaryString(i));
        if (binaryString.length() < hexString.length() * 4) {
            // 前置补充0的数量
            int supplementNum = hexString.length() * 4 - binaryString.length();
            for (int j = 0; j < supplementNum; j++) {
                binaryString.insert(0, "0");
            }
        }
        return binaryString.toString();
    }

    public static String utf16Hex2BinaryString(String hexString) {
        return Integer.toBinaryString(Integer.parseInt(hexString, 16) - Integer.parseInt("10000", 16));
    }

    public static String unicode2Utf8(String codePointHex) {
        codePointHex = parseCodePoint(codePointHex);
        Integer utf8ByteNum = getCodePointUtf8Range(codePointHex);
        String unicodeBinaryString = hex2BinaryStringWithCompensate(codePointHex);
        LOGGER.debug("UnicodeBinaryString:{}", unicodeBinaryString);
        byte[] bytes = new byte[utf8ByteNum];
        List<String> codeList = new ArrayList<>();
        if (utf8ByteNum == 1) {
            String code = "0" + unicodeBinaryString.substring(unicodeBinaryString.length() - 7);
            codeList.add(0, code);
            int integer = Integer.valueOf(code, 2);
            bytes[0] = parseInt2Byte(integer);
            LOGGER.debug("utf8 binary codes:{}", codeList);
            return new String(bytes, StandardCharsets.UTF_8);
        }
        int startIndex = unicodeBinaryString.length() - 6;
        int endIndex = unicodeBinaryString.length();
        for (int i = utf8ByteNum; i > 0; i--) {
            String prefix = "10";
            if (i == 1) {
                StringBuilder endPrefix = new StringBuilder("0");
                for (int j = 0; j < utf8ByteNum; j++) {
                    endPrefix.insert(0, "1");
                }
                String substring = unicodeBinaryString.substring(endPrefix.length() + startIndex - 2, endIndex);
                String code = endPrefix + substring;
                codeList.add(0, code);
                int integer = Integer.valueOf(code, 2);
                bytes[0] = parseInt2Byte(integer);
                break;
            }
            String code = prefix + unicodeBinaryString.substring(startIndex, endIndex);
            codeList.add(0, code);
            int integer = Integer.valueOf(code, 2);
            bytes[i - 1] = parseInt2Byte(integer);
            startIndex -= 6;
            endIndex -= 6;
        }
        LOGGER.debug("utf8 binary codes:{}", codeList);
        return new String(bytes, StandardCharsets.UTF_8);
    }

    public static String unicode2Utf16(String codePointHex) {
        codePointHex = parseCodePoint(codePointHex);
        Integer utf16ByteNum = getCodePointUtf16Range(codePointHex);
        String unicodeBinaryString = utf16Hex2BinaryString(codePointHex);
        if (utf16ByteNum == 1) {
            unicodeBinaryString = compensateFirst(unicodeBinaryString, "0", 16 - unicodeBinaryString.length());
        }
        if (utf16ByteNum == 2) {
            String firstPrefix = "110110";
            String secondPrefix = "110111";
            String compensatedStr = compensateFirst(unicodeBinaryString, "0", 20 - unicodeBinaryString.length());
            unicodeBinaryString = firstPrefix + compensatedStr.substring(0, 10) + secondPrefix + compensatedStr.substring(10);
        }
        LOGGER.debug("utf16 binary codes:{}", unicodeBinaryString);
        byte[] bytes = transfer2Bytes(unicodeBinaryString);
        return new String(bytes, StandardCharsets.UTF_16);
    }

    private static byte[] transfer2Bytes(String unicodeBinaryString) {
        int bytesNum = unicodeBinaryString.length() / 8;
        byte[] bytes = new byte[bytesNum];
        int startIndex = 0;
        int endIndex = 8;
        for (int i = 0; i < bytesNum; i++) {
            Integer integer = Integer.valueOf(unicodeBinaryString.substring(startIndex, endIndex), 2);
            bytes[i] = parseInt2Byte(integer);
            startIndex += 8;
            endIndex += 8;
        }
        return bytes;
    }

    private static byte parseInt2Byte(int integer) {
        if (integer < MIN_VALUE || integer > MAX_VALUE) {
            integer = integer - 256;
        }
        return Byte.parseByte(String.valueOf(integer));
    }

    private static String parseCodePoint(String codePointHex) {
        if (codePointHex.length() < CODE_POINT_LENGTH) {
            codePointHex = (SUPPLEMENT_NUMS + codePointHex).substring(codePointHex.length());
        } else if (codePointHex.length() > CODE_POINT_LENGTH) {
            int index = 0;
            for (int i = 0; i < codePointHex.length() - CODE_POINT_LENGTH; i++) {
                if (codePointHex.charAt(i) == '0') {
                    index = i;
                } else {
                    throw new RuntimeException("this is not a right codePoint:" + codePointHex);
                }
            }
            codePointHex = codePointHex.substring(index);
        }
        return codePointHex;
    }

    private static Integer getCodePointUtf8Range(String codePointHex) {
        return Optional.ofNullable(CODE_POINT_HEX_UTF8_RANGE_MAP.get(hex2Int(codePointHex))).orElseThrow(() -> new RuntimeException("this is not a right codePoint:" + codePointHex));
    }

    private static String compensateFirst(String targetStr, String compensateStr, Integer compensateNum) {
        StringBuilder targetStrBuilder = new StringBuilder(targetStr);
        for (int integer = 0; integer < compensateNum; integer++) {
            targetStrBuilder.insert(0, compensateStr);
        }
        return targetStrBuilder.toString();
    }

    private static String compensateEnd(String targetStr, String compensateStr, Integer compensateNum) {
        return targetStr + String.valueOf(compensateStr).repeat(Math.max(0, compensateNum));
    }

    private static Integer getCodePointUtf16Range(String codePointHex) {
        return Optional.ofNullable(CODE_POINT_HEX_UTF16_RANGE_MAP.get(hex2Int(codePointHex))).orElseThrow(() -> new RuntimeException("this is not a right codePoint:" + codePointHex));
    }

    private static int hex2Int(String hexString) {
        return Integer.valueOf(hexString, 16);
    }

    public static void main(String[] args) {
        // 表情符号😀：U+1F600
        // 汉字你：U+4F60
        System.out.println(unicode2Utf8("1F600"));
//        System.out.println(unicode2Utf16("20C30"));
    }

}
